/**
 * 
 */
package io.gitee.falllee.dao.core;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Linq4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import io.gitee.falllee.common.util.SnowflakeUtils;
import io.gitee.falllee.dao.annotation.Column;
import io.gitee.falllee.dao.annotation.Table;
import io.gitee.falllee.dao.enums.DbTypeEnum;
import io.gitee.falllee.dao.enums.KeyTypeEnum;
import io.gitee.falllee.dao.model.DaoConst;
import io.gitee.falllee.dao.model.DaoException;
import io.gitee.falllee.dao.persistence.DataMapper;
import io.gitee.falllee.dao.util.CommonUtils;
import io.gitee.falllee.dao.util.DaoUtils;

/**
 * @author batty
 * @date 2022-06-27
 *
 */
public class InsertBuilder<T> extends HeaderBuilder {
    
    private Class<T> clazz;

    private DataMapper dataMapper;
    
    private CacheBuilder<T> cacheBuilder;
    
    private MessageBuilder<T> messageBuilder;
     
    InsertBuilder(Dao parent, Class<T> clazz) {
        super(parent.getDaoConfig());
        this.clazz = clazz;
        this.dataMapper = parent.getDataMapper();
        this.cacheBuilder = parent.cacheBuilder(clazz);
        this.messageBuilder = parent.messageBuilder(clazz);
    }
    
    public InsertBuilder<T> header(String key, String value) {
        if (headerParamsMap == null) {
            headerParamsMap = new HashMap<>(16);
        }
        headerParamsMap.put(key, value);
        return this;
    }

    public Boolean insert(T t) {
        if (t == null) {
            return false;
        }
        Table table = DaoUtils.getTable(clazz);
        String tableName = table.value();
        Map<String, Object> map = new HashMap<>(16);
        boolean blSyncTable = table.syncTable();
        String syncDateField = table.syncDateField();
        Integer syncDatePrecision = table.syncDatePrecision().getPrecision();
        setInsertFieldAndValue(t, map, blSyncTable, syncDateField);
        // 非同步表组件级固定字段需要统一赋值;如果是同步表则使用实体传递的值
        if (!blSyncTable) {
            setEnableFlag(map, false);
            setCreateInfo(map);
            setUpdateInfo(map);
        } else {
            setSyncDate(map, syncDateField, syncDatePrecision);
        }
        boolean bl = insertDb(map, tableName);
        if (bl) {
            Object keyValue = map.get(DaoConst.KEY_VALUE);
            if (keyValue == null) {
                return false;
            }
            Method keyGetMethod = DaoUtils.getKeyGetMethod(clazz);
            Object key = DaoUtils.getParamValue(keyValue.toString(), keyGetMethod.getReturnType());
            DaoUtils.setKeyValue(t, key);
            // 清除实体缓存
            cacheBuilder.clearCache(null);
            // 发送Kafka消息
            messageBuilder.sendMessage(keyValue, DaoConst.INSERT);
        }
        return bl;
    }

    public Boolean insertList(List<T> list) {
        List<T> tempList = Linq4j.asEnumerable(list).where(p -> !ObjectUtils.isEmpty(p)).toList();
        if (CollectionUtils.isEmpty(list)) {
            return false;
        }
        String keyType = daoConfig.getKeyType();
        if (DbTypeEnum.ORACLE.getDbType().equals(daoConfig.getDbType())) {
            return insertList4Oracle(tempList);
        }
        Table table = DaoUtils.getTable(clazz);
        String tableName = table.value();
        Method[] methods = clazz.getMethods();
        boolean blSyncTable = table.syncTable();
        String syncDateField = table.syncDateField();
        Integer syncDatePrecision = table.syncDatePrecision().getPrecision();
        List<Map<String, Object>> listParam = getInsertValueList(tempList, keyType, methods, blSyncTable,
                syncDateField);
        Map<String, Object> map = new HashMap<>(16);
        map.put(DaoConst.DB_TYPE, daoConfig.getDbType());
        map.put(DaoConst.TABLE_NAME, tableName);
        if (!blSyncTable) {
            setEnableFlag(map, false);
            setCreateInfo(map);
            setUpdateInfo(map);
        } else {
            setSyncDate(map, syncDateField, syncDatePrecision);
        }
        String keyField = "";
        int total = tempList.size();
        int maxSize = table.insertListMaxSize();
        int num = total / maxSize;
        int left = total % maxSize;
        if (left > 0) {
            num += 1;
        }
        Integer res = 0;
        for (int i = 0; i < num; i++) {
            List<Map<String, Object>> subList = CommonUtils.getSubList(listParam, maxSize, i);
            if (KeyTypeEnum.UUID.getType().equals(keyType) || KeyTypeEnum.SNOWFLAKE_ID.getType().equals(keyType)) {
                keyField = DaoUtils.getKeyField(clazz);
                res += dataMapper.insertList(map, subList);
            } else {
                keyField = DaoConst.KEY_VALUE;
                res += dataMapper.insertAutoList(map, subList);
            }
        }
        if (res > 0) {
            // 回写主键值到list
            List<Object> keyValueList = writeBackKeyValueList(tempList, listParam, keyField);
            // 清除实体缓存
            cacheBuilder.clearCache(null);
            // 发送Kafka消息
            messageBuilder.sendMessage(keyValueList, DaoConst.INSERT_LIST);
        }
        return res > 0;
    }

    private void setInsertFieldAndValue(T t, Map<String, Object> map, boolean blSyncTable, String syncDateField) {
        Class<? extends Object> clazz = t.getClass();
        Method[] methods = clazz.getMethods();
        map.put(DaoConst.DB_TYPE, daoConfig.getDbType());
        String keyField = DaoUtils.getKeyField(clazz);
        // 记录主键字段及值
        map.put(DaoConst.KEY_FIELD, getQuoteField(keyField));
        Object keyValue = DaoUtils.getKeyValue(t);
        map.put(DaoConst.KEY_VALUE, keyValue);
        StringBuilder sbFields = new StringBuilder();
        StringBuilder sbValues = new StringBuilder();
        // 非同步表，业务固定字段从请求头设置
        if (!blSyncTable) {
            Map<String, Object> fixFieldValue = getFixedFieldValue();
            DaoUtils.setFixFieldValue(fixFieldValue, t);
        }
        List<String> configFields = getComponentConfigFields(blSyncTable, syncDateField);
        Enumerable<Method> erMethod = getValidateMethods(methods, configFields);
        for (Method method : erMethod) {
            Column col = method.getAnnotation(Column.class);
            String columnName = getColumnValue(col);
            Object objVal = DaoUtils.getValue(t, method);
            String colQuotes = getQuoteField(columnName);
            // 空值或主键不处理(在sql中组装)
            if (ObjectUtils.isEmpty(objVal) || col.key()) {
                continue;
            }
            // 组装field
            sbFields.append(colQuotes);
            sbFields.append(DaoConst.COMMA);
            // 组装value(参数化)
            Class<?> dataType = method.getReturnType();
            sbValues.append(
                    String.format("#{%s, jdbcType=%s}", columnName, DaoUtils.getJdbcType(dataType, col.jdbcType())));
            sbValues.append(DaoConst.COMMA);
            // 赋值
            map.put(columnName, objVal);
        }
        if (sbFields.length() > 0) {
            String fields = sbFields.substring(0, sbFields.length() - 1);
            map.put(DaoConst.FIELDS, fields);
        }
        if (sbValues.length() > 0) {
            String values = sbValues.substring(0, sbValues.length() - 1);
            map.put(DaoConst.VALUES, values);
        }
    }

    private boolean insertDb(Map<String, Object> map, String tableName) {
        Integer res = 0;
        map.put(DaoConst.TABLE_NAME, tableName);
        String keyType = daoConfig.getKeyType();
        map.put(DaoConst.KEY_TYPE, keyType);
        if (KeyTypeEnum.UUID.getType().equals(keyType)) {
            res = dataMapper.insertUuid(map);
        } else if (KeyTypeEnum.SNOWFLAKE_ID.getType().equals(keyType)) {
            // 如果主键值为空再填充雪花ID
            if (ObjectUtils.isEmpty(map.get(DaoConst.KEY_VALUE))) {
                long id = SnowflakeUtils.nextId();
                map.put(DaoConst.KEY_VALUE, id);
            }
            res = dataMapper.insert(map);
        } else {
            if (daoConfig.getDbType().equals(DbTypeEnum.ORACLE.getDbType())) {
                res = dataMapper.insertAuto4Oracle(map);
            } else {
                res = dataMapper.insertAuto(map);
            }
        }
        return res > 0;
    }

    private List<Map<String, Object>> getInsertValueList(List<T> list, String keyType, Method[] methods,
            boolean blSyncTable, String syncDateField) {
        List<Map<String, Object>> listParam = new ArrayList<>();
        Map<String, Object> fixFieldValue = getFixedFieldValue();
        List<String> configFields = getComponentConfigFields(blSyncTable, syncDateField);
        Enumerable<Method> erMethod = getValidateMethods(methods, configFields);
        for (int i = 0; i < list.size(); i++) {
            T t = list.get(i);
            if (!blSyncTable) {
                DaoUtils.setFixFieldValue(fixFieldValue, t);
            }
            Map<String, Object> mapValues = new HashMap<>(16);
            for (Method method : erMethod) {
                Column col = method.getAnnotation(Column.class);
                String columnName = getQuoteField(getColumnValue(col));
                Object objVal = DaoUtils.getValue(t, method);
                if (col != null && col.key()) {
                    // 如果主键值为空且主键类型为UUID的自动生成主键,自增主键清空主键值，数据层自动填充
                    objVal = DaoUtils.generateKeyValue(keyType, objVal);
                    // 回写实体对象
                    DaoUtils.setKeyValue(t, objVal);
                }
                mapValues.put(columnName, objVal);
            }
            listParam.add(mapValues);
        }
        return listParam;
    }

    private List<Object> writeBackKeyValueList(List<T> list, List<Map<String, Object>> listParam, String keyField) {
        if (CollectionUtils.isEmpty(list)) {
            return new ArrayList<>();
        }
        Class<? extends Object> clazz = list.get(0).getClass();
        Method keyGetMethod = DaoUtils.getKeyGetMethod(clazz);
        List<Object> keyValueList = new ArrayList<>();
        Class<?> keyDataType = keyGetMethod.getReturnType();
        for (int i = 0; i < list.size(); i++) {
            Map<String, Object> mapValue = listParam.get(i);
            if (mapValue.get(keyField) != null) {
                Object obj = DaoUtils.getParamValue(mapValue.get(keyField).toString(), keyDataType);
                DaoUtils.setKeyValue(list.get(i), obj);
                keyValueList.add(obj);
            }
        }
        return keyValueList;
    }

    private Boolean insertList4Oracle(List<T> list) {
        Class<? extends Object> clazz = list.get(0).getClass();
        Table table = DaoUtils.getTable(clazz);
        String tableName = table.value();
        Method[] methods = clazz.getMethods();
        Map<String, Object> map = new HashMap<>(16);
        map.put(DaoConst.DB_TYPE, daoConfig.getDbType());
        boolean blSyncTable = table.syncTable();
        String syncDateField = table.syncDateField();
        Integer syncDatePrecision = table.syncDatePrecision().getPrecision();
        List<String> configFields = getComponentConfigFields(blSyncTable, syncDateField);
        Enumerable<Method> erMethod = getValidateMethods(methods, configFields);
        String keyType = daoConfig.getKeyType();
        Map<String, Method> mapSetMethod = setFieldAndValue(methods, erMethod, map);
        List<Object> keyValueList = setFieldAndValueList(list, tableName, blSyncTable, erMethod, keyType, mapSetMethod);
        map.put(DaoConst.TABLE_NAME, tableName);
        if (!blSyncTable) {
            setEnableFlag(map, false);
            setCreateInfo(map);
            setUpdateInfo(map);
        } else {
            setSyncDate(map, syncDateField, syncDatePrecision);
        }
        int total = list.size();
        int maxSize = table.insertListMaxSize();
        int num = total / maxSize;
        int left = total % maxSize;
        if (left > 0) {
            num += 1;
        }
        Integer res = 0;
        for (int i = 0; i < num; i++) {
            List<T> subList = CommonUtils.getSubList(list, maxSize, i);
            res += dataMapper.insertList4Oracle(map, subList);
        }
        if (res > 0) {
            // 清除实体缓存
            cacheBuilder.clearCache(null);
            // 发送Kafka消息
            messageBuilder.sendMessage(keyValueList, DaoConst.INSERT_LIST);
        }
        return res > 0;
    }

    private Map<String, Method> setFieldAndValue(Method[] methods, Enumerable<Method> erMethod,
            Map<String, Object> map) {
        StringBuilder sbFields = new StringBuilder();
        StringBuilder sbValues = new StringBuilder();
        Map<String, String> mapJdbc = new HashMap<>(16);
        Map<String, String> mapField = new HashMap<>(16);
        Map<String, Method> mapSetMethod = new HashMap<>(16);
        for (Method method : erMethod) {
            Column col = method.getAnnotation(Column.class);
            String columnName = getColumnValue(col);
            String colQuotes = getQuoteField(columnName.toUpperCase());
            if (sbFields.toString().contains(colQuotes)) {
                continue;
            }
            Class<?> dataType = method.getReturnType();
            mapJdbc.put(columnName, DaoUtils.getJdbcType(dataType, col.jdbcType()));
            String getMethodName = method.getName();
            // 获取方法对应属性名称
            String fieldName = DaoUtils.getFieldNameByMethodName(getMethodName);
            mapField.put(columnName, fieldName);
            // 获取Set方法
            String setMethodName = getMethodName.replaceFirst("get", "set");
            Method setMethod = Linq4j.asEnumerable(methods).singleOrDefault(p -> p.getName().equals(setMethodName));
            mapSetMethod.put(columnName, setMethod);
            // 组装field
            sbFields.append(colQuotes);
            sbFields.append(DaoConst.COMMA);
            // 组装value(参数化)
            sbValues.append(
                    String.format("#{item.%s, jdbcType=%s}", mapField.get(columnName), mapJdbc.get(columnName)));
            sbValues.append(DaoConst.COMMA);
        }
        String fields = "";
        String values = "";
        if (sbFields.length() > 0) {
            fields = sbFields.substring(0, sbFields.length() - 1);
            map.put("fields", fields);
        } else {
            throw new DaoException(DaoConst.NO_FIELDS_MAPPING);
        }
        if (sbValues.length() > 0) {
            values = sbValues.substring(0, sbValues.length() - 1);
            // 注:这里key不能用values,values为map的属性
            map.put("vals", values);
        }
        return mapSetMethod;
    }
    
    private List<Object> setFieldAndValueList(List<T> list, String tableName, boolean blSyncTable,
            Enumerable<Method> erMethod, String keyType, Map<String, Method> mapSetMethod) {
        Map<String, Object> fixFieldValue = getFixedFieldValue();
        List<Object> keyValueList = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            T t = list.get(i);
            if (!blSyncTable) {
                DaoUtils.setFixFieldValue(fixFieldValue, t);
            }
            for (Method method : erMethod) {
                Column col = method.getAnnotation(Column.class);
                String columnName = getColumnValue(col);
                Object objVal = DaoUtils.getValue(t, method);
                if (col != null && col.key()) {
                    // 如果主键值为空且主键类型为UUID的自动生成主键,自增主键清空主键值，数据层自动填充
                    objVal = getOracleKeyValue(tableName, keyType, objVal);
                    keyValueList.add(objVal);
                }
                DaoUtils.setValue(t, mapSetMethod.get(columnName), objVal);
            }
        }
        return keyValueList;
    }

    private Object getOracleKeyValue(String tableName, String keyType, Object objVal) {
        if (ObjectUtils.isEmpty(objVal)) {
            if (KeyTypeEnum.UUID.getType().equals(keyType)) {
                objVal = CommonUtils.getGuid();
            } else if (KeyTypeEnum.SNOWFLAKE_ID.getType().equals(keyType)) {
                objVal = SnowflakeUtils.nextId();
            }
        } else if (KeyTypeEnum.AUTO.getType().equals(keyType)) {
            String sequence = String.format("%s_S", tableName);
            objVal = dataMapper.getOracleSerialNumber(sequence);
        }
        return objVal;
    }
}
